/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.community.component.controls;

import cn.ac.nercis.pes.community.component.skin.PesCheckComboBoxSkin;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.util.StringConverter;
import org.controlsfx.control.IndexedCheckModel;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定义多选下拉框
 * @author zpy
 * @version 1.0.0
 */
public class PesCheckComboBox<T> extends Control {
    private final ObservableList<T> items;
    private final Map<T, BooleanProperty> itemBooleanMap;
    private PesCheckComboBoxSkin<T> checkComboBoxSkin;
    private String joinChar;

    public PesCheckComboBox() {
        this(null,",");
    }
    public PesCheckComboBox(final ObservableList<T> items,final String joinChar) {
        final int initialSize = items == null ? 32 : items.size();
        this.joinChar = joinChar;

        this.itemBooleanMap = new HashMap<>(initialSize);
        this.items = items == null ? FXCollections.observableArrayList() : items;
        setCheckModel(new CheckComboBoxBitSetCheckModel<>(this.items, itemBooleanMap));
    }
    /**
     * Represents the list of choices available to the user, from which they can
     * select zero or more items.
     */
    public ObservableList<T> getItems() {
        return items;
    }

    /**
     * Returns the {@link BooleanProperty} for a given item index in the
     * CheckComboBox. This is useful if you want to bind to the property.
     */
    public BooleanProperty getItemBooleanProperty(int index) {
        if (index < 0 || index >= items.size()) {
            return null;
        }
        return getItemBooleanProperty(getItems().get(index));
    }

    public String getJoinChar(){
        return this.joinChar;
    }

    public void setJoinChar(String joinChar){
        this.joinChar = joinChar;
        if(Objects.nonNull(this.checkComboBoxSkin)){
            this.checkComboBoxSkin.setJoinChar(joinChar);
        }
    }


    /**
     * Returns the {@link BooleanProperty} for a given item in the
     * CheckComboBox. This is useful if you want to bind to the property.
     */
    public BooleanProperty getItemBooleanProperty(T item) {
        return itemBooleanMap.get(item);
    }

    private final ObjectProperty<IndexedCheckModel<T>> checkModel = new SimpleObjectProperty<>(this, "checkModel");

    public final void setCheckModel(IndexedCheckModel<T> value) {
        checkModelProperty().set(value);
    }

    /**
     * Returns the currently installed check model.
     */
    public final IndexedCheckModel<T> getCheckModel() {
        return checkModel == null ? null : checkModel.get();
    }

    /**
     * The check model provides the API through which it is possible
     * to check single or multiple items within a CheckComboBox, as  well as inspect
     * which items have been checked by the user. Note that it has a generic
     * type that must match the type of the CheckComboBox itself.
     */
    public final ObjectProperty<IndexedCheckModel<T>> checkModelProperty() {
        return checkModel;
    }

    private final ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<StringConverter<T>>(this, "converter");

    /**
     * A {@link StringConverter} that, given an object of type T, will
     * return a String that can be used to represent the object visually.
     */
    public final ObjectProperty<StringConverter<T>> converterProperty() {
        return converter;
    }

    /**
     * Sets the {@link StringConverter} to be used in the control.
     * @param value A {@link StringConverter} that, given an object of type T, will
     * return a String that can be used to represent the object visually.
     */
    public final void setConverter(StringConverter<T> value) {
        converterProperty().set(value);
    }

    /**
     * A {@link StringConverter} that, given an object of type T, will
     * return a String that can be used to represent the object visually.
     */
    public final StringConverter<T> getConverter() {
        return converterProperty().get();
    }

    private final StringProperty title = new SimpleStringProperty(null);

    /**
     * The title to use for this control. If a non null value is explicitly
     * set by the client, then that string will be used, otherwise a title
     * will be constructed concatenating the selected items
     */
    public final StringProperty titleProperty() {
        return title;
    }

    /**
     * Sets the title to use. If it is not null it will be used as title,
     * otherwise title will be constructed by the skin
     * @param value the string to use as title
     */
    public final void setTitle(String value) {
        title.setValue(value);
    }

    /**
     * The title set for this control, if it has been set explicitly by the client.
     * @return the title if it has been set, null otherwise
     */
    public final String getTitle() {
        return title.getValue();
    }

    /**
     * Requests that the ComboBox display the popup aspect of the user interface.
     */
    public void show() {
        if (checkComboBoxSkin != null) {
            checkComboBoxSkin.show();
        }
    }

    /**
     * Closes the popup / dialog that was shown when {@link #show()} was called.
     */
    public void hide() {
        if (checkComboBoxSkin != null) {
            checkComboBoxSkin.hide();
        }
    }

    /** {@inheritDoc} */
    @Override protected Skin<?> createDefaultSkin() {
        checkComboBoxSkin = new PesCheckComboBoxSkin<>(this,getJoinChar());
        return checkComboBoxSkin;
    }

    private final BooleanProperty showCheckedCount = new SimpleBooleanProperty(false);

    /**
     * A boolean to decide if the information of how many items are checked
     * should be shown beside the fixed title.
     * If a {@link #titleProperty()} has been set and this property is set to true
     * then a string like (3/10) would be shown when 3 items out of 10 are
     * checked.<br>
     * This property has effect only if a fixed title has been set (see {@link #titleProperty()}),
     * otherwise the title is constructed with a concatenation of the selected items.
     *
     * @return if the count should be shown
     */
    public final BooleanProperty showCheckedCountProperty() {
        return showCheckedCount;
    }

    /**
     * Sets the value to use to decide whether the checked items count should be
     * shown or not
     * @param value the value to set
     */
    public final void setShowCheckedCount(boolean value) {
        showCheckedCount.setValue(value);
    }

    /**
     * @return whether the checked items count is set to be shown beside a fixed title
     */
    public final boolean isShowCheckedCount() {
        return showCheckedCount.getValue();
    }

    private static class CheckComboBoxBitSetCheckModel<T> extends CheckBitSetModelBase<T> {

        private final ObservableList<T> items;

        CheckComboBoxBitSetCheckModel(final ObservableList<T> items, final Map<T, BooleanProperty> itemBooleanMap) {
            super(itemBooleanMap);

            this.items = items;
            this.items.addListener((ListChangeListener<T>) c -> updateMap());

            updateMap();
        }

        @Override public T getItem(int index) {
            return items.get(index);
        }

        @Override public int getItemCount() {
            return items.size();
        }

        @Override public int getItemIndex(T item) {
            return items.indexOf(item);
        }
    }
}